<?php
/**
 * @author Jumper
 * date: 2021/04/16
 */
declare(strict_types=1);
namespace App\Chat\Core;

use Hyperf\Redis\Redis;
use App\Constants\ErrorCode;
use App\Log;

class Room
{
    const USER_LOGIN = 1;   //用户登陆
    const USER_ENTER = 2;   //用户进入
    const USER_LEAVE = 3;   //用户离开
    const USER_SAY = 4;     //用户讲话
    
    /**
     * 用户信息缓存key user:info:{fd}
     */
    private $user_info_key = "user:info:%d";

    /**
     * 房间内用户连接列表缓存key  room:users:{roomid}  用set保存
     */
    private $room_users_key = "room:users:%d";

    /**
     * 房间列表
     */
    private $rooms = [];

    /**
     * redis链接
     */
    private $redis;

    private $methods = [
        self::USER_LOGIN => 'userLogin',
        self::USER_ENTER => 'userEnter',
        self::USER_LEAVE => 'userLeave',
        self::USER_SAY => 'userSay',
    ];
    
    public function __construct(Redis $redis)
    {
        $this->redis = $redis;
        $this->rooms = config('chat.room');
    }
    
    public function exec($server, int $fd, array $data) : void
    {
        if (!isset($data['method']) || !array_key_exists($data['method'], $this->methods)) {
            return;
        }

        $method = $this->methods[$data['method']];
        $this->$method($server, $fd, $data);
    }

    /**
     * 广播消息
     */
    protected function broadcastMessage($server, array $users, array $data) : void
    {
        $data = Packet::encode(Packet::format(ErrorCode::SUCCESS, '', $data));
        foreach($users as $_fd) {
            $server->push(intval($_fd), $data, WEBSOCKET_OPCODE_BINARY);
        }
    }

    /**
     * 用户登陆
     */
    protected function userLogin($server, int $fd, array $data)
    {
        $info = [
            'name' => $data['name'],
            'room' => 0,
            'fd' => $fd,
        ];
        $this->setUserInfo($info);
        Log::info($info['fd'] . ' login with:' . $info['name']);
    }

    /**
     * 用户进入房间,广播消息用户进入消息
     */
    protected function userEnter($server, int $fd, array $data) : void
    {
        $roomId = intval($data['room']);
        if (!in_array($roomId, $this->rooms)) {
            $this->sendError($server, $fd, '房间' . $roomId . '不存在');
        }
        $info = $this->getUserInfo($fd);
        if ($info['room'] > 0 && $info['room'] != $roomId) {
            $this->userLeave($server, $fd);
        }
        
        $this->addToRoom($roomId, $fd);
        $users = $this->getRoomUsers($roomId);
        
        $data = array(
            'method' => self::USER_ENTER,
            'name' => $info['name'],
            'content' => '用户进入房间',
        );
        $this->broadcastMessage($server, $users, $data);
        Log::info($info['fd'] . ' enter room:' . $roomId);
    }
    
    /**
     * 用户离开房间,广播消息用户离开消息
     */
    protected function userLeave($server, int $fd, array $data = []) : void
    {
        $info = $this->getUserInfo($fd);
        $roomId = $info['room'];
        if (!in_array($roomId, $this->rooms)) {
            $this->sendError($server, $fd, '房间' . $roomId . '不存在');
        }
        $this->removeFromRoom($roomId, $fd);
        $users = $this->getRoomUsers($roomId);
        
        $data = array(
            'method' => self::USER_LEAVE,
            'name' => $info['name'],
            'content' => '用户离开房间',
        );
        $this->broadcastMessage($server, $users, $data);
        Log::info($info['fd'] . ' leave room:' . $roomId);
    }

    /**
     * 用户发布消息
     */
    protected function userSay($server, int $fd, array $data)
    {
        $info = $this->getUserInfo($fd);
        $roomId = $info['room'];
        if (!in_array($roomId, $this->rooms)) {
            $this->sendError($server, $fd, '房间' . $roomId . '不存在');
            return;
        }

        $users = $this->getRoomUsers($roomId);
        $data = [
            'method' => self::USER_SAY,
            'name' => $info['name'],
            'content' => $data['content'],
        ];
        $this->broadcastMessage($server, $users, $data);
        Log::info($info['fd'] . ' say:' . $data['content']);
    }

    /**
     * 查找用户信息
     * @param int   $fd
     * @return array
     */
    protected function getUserInfo(int $fd) : array
    {
        $key = sprintf($this->user_info_key, $fd);
        $info = $this->redis->get($key);
        if (empty($info)) {
            return [
                'name' => '匿名用户',
                'room' => 0,
                'fd' => $fd,
            ];
        }

        return json_decode($info, true);
    }

    /**
     * 保存用户信息
     * @param array $info
     */
    protected function setUserInfo(array $info) : void
    {
        if (!isset($info['fd'])) return;
        $key = sprintf($this->user_info_key, $info['fd']);
        $this->redis->set($key, json_encode($info));
    }

    /**
     * 读取相关房间的用户连接列表
     * @param int   $roomId
     * @return array
     */
    protected function getRoomUsers(int $roomId) : array
    {
        $key = sprintf($this->room_users_key, $roomId);
        $users = $this->redis->smembers($key);
        if (empty($users)) {
            return [];
        }
        return $users;
    }

    /**
     * 将连接加入房间保存
     * @param int   $roomId
     * @param int   $fd
     */
    protected function addToRoom(int $roomId, int $fd) : void
    {
        $key = sprintf($this->room_users_key, $roomId);
        $this->redis->sadd($key, $fd);

        $info = $this->getUserInfo($fd);
        $info['room'] = $roomId;
        $this->setUserInfo($info);
    }
    
    /**
     * 将连接从房间中去除
     * @param int   $roomId
     * @param int   $fd
     */
    protected function removeFromRoom(int $roomId, int $fd)
    {
        $key = sprintf($this->room_users_key, $roomId);
        $this->redis->srem($key, $fd);

        $info = $this->getUserInfo($fd);
        $info['room'] = 0;
        $this->setUserInfo($info);
    }

    /**
     * 返回错误消息给用户
     */
    protected function sendError($server, int $fd, string $message)
    {
        $info = $this->getUserInfo($fd);
        $data = Packet::encode(Packet::format(ErrorCode::DATA_ERROR, $message, []));
        $server->push($fd, $data, WEBSOCKET_OPCODE_BINARY);
    }
}